package Group9.TodoManager.Data;

import Group9.TodoManager.Data.Interface.ITableModel;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.EventListenerList;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;

/**
 * This class extends an abstract table model so that it can be set as model to
 * JTable. This class provides support for doing basic things like adding row,
 * adding columns, getting values etc. This class main purpose is to allow
 * setting a data source to this model with a collection of items T. So, this
 * works for any kind of objects which is the purpose of using generic here.
 * @author Rupesh
 * @author volkan cambazoglu
 */
public class SimpleTableModel<T> extends AbstractTableModel implements ITableModel<T> {
	
    private int selectedRowIndex;

    private List<String> columns;

    private EventListenerList listenerList = new EventListenerList();

    private PropertyChangeEvent changeEventObject;
    
    //STORES THE MAPPING BETWEEN THE COLUMN DISPLAY NAME AND THE PROPERTY NAME
    //IN THE OBJECT THAT IS SET AS DATASOURCE
    private HashMap<String, String> columnFieldMapping;

    //THE LIST OF OBJECTS SET AS DATASOURCE STORED AS AN OBJECT OF TABLEROW
    private List<TableRow<T>> rowsObject;

    //THE HASHMAP WHICH KEEPS THE MAPPING BETWEEN THE OBJECT PROPERTY AND THE
    //ACCESSOR METHOD OF THE OBJECT TO REFLECT THEIR VALUE TO SHOW IN TABLE
    private HashMap<String, Method> propertyAccessorMethods;

    private boolean isCellEditable = false;

    //LIST WHICH HOLDS THE LIST OF MODEL LISTENERS
    private List<TableModelListener> modelListeners = 
            new ArrayList<TableModelListener>();

    /**
     * constructor for creating an instance of the table model.
     */
    public SimpleTableModel() {
        columns = new ArrayList<String>();
        rowsObject = new ArrayList<TableRow<T>>();
        columnFieldMapping = new HashMap<String, String>();
        selectedRowIndex = -1;
    }

    /**
     * Setter method for the object at the specified row index
     * @param rowIndex the index of the row
     * @param object object to be set at the specified row index
     */
    public void setRowObject(int rowIndex, T object) {
        rowsObject.get(selectedRowIndex).updateRowValue(object);
        fireTableRowsUpdated(rowIndex, rowIndex);
    }

    /**
     * Adds listener to the table model in case some property is changed
     * @param l the listener to be added
     */
    public void addPropertyChangeListener(PropertyChangeListener l) {
        listenerList.add(PropertyChangeListener.class, l);
    }

    /**
     * Removes listener from the table model in case it is added before
     * @param l the listener to be removed
     */
    public void removePropertyChangeListener(PropertyChangeListener l) {
        listenerList.remove(PropertyChangeListener.class, l);
    }

    /**
     * Fire an event about change of property
     * All added listeners will be notified of the change of property by
     * this method
     * @param event event of property change
     */
    public void firePropertyChangedEvent(PropertyChangeEvent e) {
        for(Object l : listenerList.getListenerList())
            if(l instanceof PropertyChangeListener)
                ((PropertyChangeListener)l).propertyChange(e);
    }

    /**
     * Returns the selected index of the row in the table
     * @return
     */
    public int getSelectedRowIndex() {
        return selectedRowIndex;
    }

    /**
     * This is the setter method for selecting a row
     * @param selectedRowIndex
     */
    public void setSelectedRowIndex(int selectedRowIndex) {
        if(this.selectedRowIndex != selectedRowIndex) {
            int oldvalue = this.selectedRowIndex;
            this.selectedRowIndex = selectedRowIndex;
            firePropertyChangedEvent(new PropertyChangeEvent(this, 
            		"SelectedRowIndex", oldvalue, selectedRowIndex));
        }
    }

    /**
     * INNERCLASS TO STORE THE OBJECT AND THE EXTRACTED VALUE TO SHOW
     * IN THE TABLE FROM THE OBJECT T.
     * @param <T> THE OBJECT THAT IS FED AS A DATA SOURCE ITEM TO SHOW IN THE
     * TABLE
     */
    class TableRow<T> {
        private List<Object> rowValues;
        private T rowObject;
        private HashMap<String, Method> propertyAccessors;

        /**
         *
         * @return returns the object instance that is being shown in the row
         */
        public T getRowObject() {
            return rowObject;
        }

        /**
         * This method is used to get the data object that is being shown in the
         * table by index.
         * @param index The index of the dataobject
         * @return The dataobject that is shown in the row
         */
        public Object getValueAt(int index) {
            return rowValues.get(index);
        }

        /**
         * This is a class that represents a row in table
         * @param rowObject The object that is being shown in the table in a row
         * @param propertyAccessors The list of methods used to extract the
         * properties value from the object.
         */
        public TableRow(T rowObj, HashMap<String, Method> propertyAccess) {
            rowObject = rowObj;
            rowValues = new ArrayList<Object>();
            propertyAccessors = propertyAccess;

            updateRowValue(rowObject);
        }

        /**
         * Update value of the object at the row
         * @param rowObject
         */
        public final void updateRowValue(T rowObject) {
            rowValues.clear();
            for(String column : columns) {
                try {
                    rowValues.add(propertyAccessors.get(column).invoke(rowObject).toString());
                } catch (IllegalAccessException ex) {
                    Logger.getLogger(SimpleTableModel.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalArgumentException ex) {
                    Logger.getLogger(SimpleTableModel.class.getName()).log(Level.SEVERE, null, ex);
                } catch (InvocationTargetException ex) {
                    Logger.getLogger(SimpleTableModel.class.getName()).log(Level.SEVERE, null, ex);
                }
                catch(Exception e) {
                    System.err.println(e.getStackTrace());
                }
            }
        }
    }

    /**
     * This stores the column values and the column display names.
     * @param c The object that is being showed.
     * @param propertyList The list of properties of the object that needs to
     * be shown in the UI.
     * @return returns the mapping between column name and column display name.
     */
    private HashMap<String, Method> getPropertyAccessorMethods(T c,
            									Iterable<String> propertyList) {
    	
        //THE HASHMAP TO STORE THE MAPPING BETWEEN COLUMN DISPLAY NAME AND THE
        //PROPERTY ACCESSOR METHODS REFLECTED FROM THE OBJECT
        HashMap<String, Method> columnMethodMapping = new HashMap<String, Method>();        
        for (Method m : c.getClass().getMethods()) {           
            for (String property : propertyList) {
                //TRY TO GET THE PROPERTY ACCESSOR METHODS USING GET PREFIX
                //FOR EG. IF I HAVE A PROPERTY NAMED CATEGORY THEN THE CONVENTION
                //FOLLOWED IS THAT THE CATEGORY ACCESSOR METHOD IN THE OBJECT
                //WILL BE GETCATEGORY SO IT WILL SEARCH FOR GETCATEGORY METHOD
                //IN THE OBJECT 
                if (m.getName().toUpperCase().trim().equals("GET" + 
                					property.toUpperCase().trim())) {
                    columnMethodMapping.put(property, m);
                }
            }
        }
        return columnMethodMapping;
    }

    /**
     * Returns the column at the specified index.
     * @param index the index of the column.
     * @return the column name at the specified index.
     */
    public String getColumnAt(int index) {
        return columns.get(index);
    }

    /**
     * This returns the total rows in the table.
     * @return Totoal number of rows in the table
     */
    public int getRowCount() {
        return rowsObject.size();
    }

    /**
     * Returns the total number of columns.
     * @return The total number of columns in table.
     */
    public int getColumnCount() {
        return columns.size();
    }

    /**
     * Returns the value at the specified row and column index.
     * @param rowIndex The rowindex to get the value
     * @param columnIndex The columnindex to get the value
     * @return The value at the specified cell
     */
    public Object getValueAt(int rowIndex, int columnIndex) {
        return rowsObject.get(rowIndex).getValueAt(columnIndex);
    }
    
	/**
	 * Returns the column name for the specified index.
	 * @param column The column index.
	 * @return The column name for the specified column index.
	 */
    @Override
    public String getColumnName(int column) {
        return columnFieldMapping.get(columns.get(column));
    }

    /**
     * Adds a column to the table
     * @param columnName The columnname to add in the table.
     */
    public void addColumn(String columnName) {
        addColumns(new String[] {columnName});
    }

    /**
     * Adds the column with specified column name and display value.
     * @param columnDisplayName The column name that is to be displayed in the
     * header of the table in the view.
     * @param propertyName The property name that maps to the object.
     */
    public void addColumn(String columnDisplayName, String propertyName) {
        columns.add(propertyName);
        columnFieldMapping.put(propertyName, columnDisplayName);
    }

    /**
     * Adds the provided column list to the table in view. It consider the
     * column name and column display value to be same.
     * @param columnsList The list of columns to add in the table.
     */
    public void addColumns(String[] columnsList) {

        for(String column : columnsList) {
            addColumn(column, column);
        }

        columns.addAll(Arrays.asList(columnsList));
        fireTableStructureChanged();
    }

    /**
     * Adds the specified list of columns and display names in the table.
     * @param columnsDisplayList The list of column display names
     * @param propertyList The list of columns name
     */
    public void addColumns(String[] columnsDisplayList,String[] propertyList) {
        int length = columnsDisplayList.length;

        for(int i = 0; i < length; i++) {
            addColumn(columnsDisplayList[i], propertyList[i]);
        }
    }

    /**
     * Returns the list of columns being shown in the table.
     * @return The list of columns shown in the table.
     */
    public List<String> getColumns() {
        return columns;
    }

    /**
     * This adds a row in the table.
     * @param task The object that is to be added to the table.
     */
    public void addRow(T task) {
        int rowIndex = rowsObject.size();
        if(propertyAccessorMethods == null || propertyAccessorMethods.isEmpty()) {
        	propertyAccessorMethods = getPropertyAccessorMethods(task, columns);
        }
        rowsObject.add(new TableRow<T>(task, propertyAccessorMethods));        
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    /**
     * This feeds the table with the list of objects provided.
     * @param taskList The list of objects to be added in the table rows.
     */
    public void setDataSource(Iterable<T> taskList) {
        clearRows();
        rowsObject.clear();
        for(T task : taskList){
        	addRow(task);
        }
        if(getRowCount() > 0) {
            fireTableDataChanged();
            fireTableStructureChanged();
            setSelectedRowIndex(0);
        }
        else {
            setSelectedRowIndex(-1);
        }
    }

    /**
     * Returns the object associated with the specified row index.
     * @param index The row index to get the object.
     * @return The row object being shown for the specified row index.
     */
    public T getRowObject(int index) {
        return rowsObject.get(index).getRowObject();
    }

    /**
     * Removes the specified row index object from the table.
     * @param index The row index for which the row is to be removed.
     */
    public void removeRowAt(int index) {
        rowsObject.remove(index);
        fireTableRowsDeleted(index, index);
    }

    /**
     * It clears all the rows from the table.
     */
    public void clearRows() {
        int rowCount = rowsObject.size();
        if(rowCount > 0) {
            rowsObject.clear();
            fireTableRowsDeleted(0, rowCount - 1);
        }
    }
}